package net.entelijan.cobean.bind.impl;

import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import net.entelijan.cobean.bind.IBeanProperty;
import net.entelijan.cobean.bind.IBinding;
import net.entelijan.cobean.bind.IColumn;
import net.entelijan.cobean.bind.IColumnBinding;
import net.entelijan.cobean.bind.IItemWrapper;
import net.entelijan.cobean.bind.IModelChangeListener;
import net.entelijan.cobean.bind.IPropertyBindingStrategy;
import net.entelijan.cobean.bind.IRemovableFactory;
import net.entelijan.cobean.core.IRemovable;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.ClassUtils;

public class SpringPropertyBindingStrategy implements IPropertyBindingStrategy {

    private static Log log = LogFactory.getLog(SpringPropertyBindingStrategy.class);
    
    private IModelWrapper modelWrapper = new DefaultModelWrapper();

    public SpringPropertyBindingStrategy() {
        super();
    }

    public <T, C> T bind(T model, IBinding<C> propertyBinding, C comp) {
        log.debug("[bind](0) model='" + model + "' binding='" + propertyBinding + "' comp='" + comp
                + "'");
        T re;
        if (model == null) {
            re = null;
        } else if (model instanceof Enum<?>) {// Do not wrap Enums as they cannot be changed.
            re = model;
        } else {
            String modelProperty = propertyBinding.getModelProperty();
            IModelChangeListener modelChangeListener;
            List<IModelChangeListener> modelChangeListeners;
            CollectionRemovingFactory<IModelChangeListener> removingFactory = new CollectionRemovingFactory<IModelChangeListener>();
            if (CobeanSpringUtil.isWrapped(model)) {
                modelChangeListeners = CobeanSpringUtils.getModelChangeListener(model);
                modelChangeListener = propertyBinding.createModelChangeListener(createBeanProperty(
                        model, modelProperty), comp, removingFactory);
                re = model;
            } else {
                modelChangeListeners = new ArrayList<IModelChangeListener>();
                re = modelWrapper.wrapModel(model, modelChangeListeners);
                log.debug("[bind](1) wrapped model '" + model + "'");
                modelChangeListener = propertyBinding.createModelChangeListener(createBeanProperty(
                        re, modelProperty), comp, removingFactory);
            }
            modelChangeListener.updateComponent();
            removingFactory.setColl(modelChangeListeners);
            removingFactory.setObj(modelChangeListener);
            modelChangeListeners.add(modelChangeListener);
            log.debug("[bind](2) added new model change listener '" + modelChangeListener
                    + "' to model '" + model + "' listeners=" + modelChangeListeners);
        }
        return re;
    }

    public <T> IBeanProperty createBeanProperty(T model, String modelProperty) {
        SpringBeanProperty re = new SpringBeanProperty(model, modelProperty);
        return re;
    }

    @Override
    public String toString() {
        return SpringPropertyBindingStrategy.class.getName();
    }

    public <C> IItemWrapper createItemWrapper(Object obj, List<IColumnBinding<C>> itemBindings) {
        return new SpringPropertyItemWrapper(itemBindings, obj);
    }

    public <C> IColumn createColumn(IColumnBinding<C> columnBinding) {
        return new SpringColumn(columnBinding);
    }

    public <C> void bindColumns(Collection<Object> coll, List<IColumnBinding<C>> columnBindings,
            C comp) {
        log.debug("[bindColumns] coll:" + coll + " columnBindings:" + columnBindings + " comp:"
                + comp);
        ArrayList<Object> tmp = new ArrayList<Object>(coll);
        coll.clear();
        Collection<Object> target = CobeanSpringUtil.getTarget(coll);
        for (Object row : tmp) {
            for (IColumnBinding<C> cb : columnBindings) {
                row = bind(row, cb, comp);
            }
            target.add(row);
        }
    }

    @Override
    public List<IColumnBinding<? extends Object>> getBeanBasedColumnBindings(Class<?> clazz) {
        if (CobeanSpringUtil.isProxy(clazz)) {
            clazz = CobeanSpringUtil.getOriginalClass(clazz);
        }
        log.info("[getBeanBasedColumnBindings] clazz:" + clazz);
        ArrayList<IColumnBinding<? extends Object>> re = new ArrayList<IColumnBinding<? extends Object>>();
        BeanWrapper bw = new BeanWrapperImpl(clazz);
        for (PropertyDescriptor pd : bw.getPropertyDescriptors()) {
            Class<?> propertyType = pd.getPropertyType();
            if (propertyType != null) {
                if (ClassUtils.isAssignable(Number.class, propertyType)) {
                    BoundPropertyColumnBinding cbind = new BoundPropertyColumnBinding();
                    cbind.setModelProperty(pd.getName());
                    cbind.setName(pd.getName());
                    re.add(cbind);
                } else if (ClassUtils.isPrimitiveOrWrapper(propertyType)) {
                    BoundPropertyColumnBinding cbind = new BoundPropertyColumnBinding();
                    cbind.setModelProperty(pd.getName());
                    cbind.setName(pd.getName());
                    re.add(cbind);
                } else if (ClassUtils.isAssignable(String.class, propertyType)) {
                    BoundPropertyColumnBinding cbind = new BoundPropertyColumnBinding();
                    cbind.setModelProperty(pd.getName());
                    cbind.setName(pd.getName());
                    re.add(cbind);
                }
            }
        }
        return re;
    }

    private static class CollectionRemovingFactory<T> implements IRemovableFactory {

        private Collection<T> coll;

        private T obj;

        @Override
        public IRemovable createRemovable() {
            return new IRemovable() {

                @Override
                public void remove() {
                    if (coll == null) {
                        throw new IllegalStateException("'coll' must not be null");
                    }
                    if (obj == null) {
                        throw new IllegalStateException("'obj' must not be null");
                    }
                    boolean re = coll.remove(obj);
                    log.debug("[remove] removed " + re + " '" + obj + "' from " + coll);
                }
            };
        }

        public void setColl(Collection<T> coll) {
            this.coll = coll;
        }

        public void setObj(T obj) {
            this.obj = obj;
        }

        @Override
        public String toString() {
            return "CollectionRemovingFactory [coll=" + coll + ", obj=" + obj + "]";
        }

    }

    public IModelWrapper getModelWrapper() {
        return modelWrapper;
    }

    public void setModelWrapper(IModelWrapper modelWrapper) {
        this.modelWrapper = modelWrapper;
    }
}
